use crate::{
    action::user::{create_user, CreateError, UpdatePasswordError, UserIdExt},
    model::user::{PasswordSecret, UserInfo, Username},
};
use rocket::{
    fairing,
    fairing::{Fairing, Info, Kind},
    Build, Rocket,
};
use rocket_db_pools::{
    sqlx::{self, Row},
    Database,
};

#[derive(Database)]
#[database("user")]
pub struct UserDb(sqlx::SqlitePool);

pub struct UserDbInitializer;

async fn has_any_user(db: &UserDb) -> Result<bool, sqlx::Error> {
    sqlx::query("SELECT EXISTS (SELECT 1 FROM users)")
        .fetch_one(&db.0)
        .await
        .map(|r| r.try_get(0))?
}

async fn create_user_table(db: &UserDb) -> Result<(), sqlx::Error> {
    sqlx::query(
        r#"
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT NOT NULL UNIQUE,
                password_hash BLOB,
                password_salt BLOB,
                is_admin BOOLEAN NOT NULL
            )
        "#,
    )
    .execute(&db.0)
    .await
    .map(|_| ())
}

async fn insert_initial_user(db: &UserDb) -> Result<(), InitializeError> {
    let mut transaction = db.begin().await?;
    let user_data = create_user(
        &mut transaction,
        Username(String::from("admin")),
        UserInfo { is_admin: true },
    )
    .await?;
    user_data
        .identifier
        .id
        .update_user_password(&mut transaction, PasswordSecret("123456".to_string()))
        .await?;
    transaction.commit().await?;
    Ok(())
}

#[derive(Debug)]
#[allow(dead_code)]
enum InitializeError {
    CreateTable(sqlx::Error),
    CreateUser(CreateError),
    UpdateCredentials(UpdatePasswordError),
}

impl From<sqlx::Error> for InitializeError {
    fn from(err: sqlx::Error) -> Self {
        InitializeError::CreateTable(err)
    }
}

impl From<CreateError> for InitializeError {
    fn from(err: CreateError) -> Self {
        InitializeError::CreateUser(err)
    }
}

impl From<UpdatePasswordError> for InitializeError {
    fn from(err: UpdatePasswordError) -> Self {
        InitializeError::UpdateCredentials(err)
    }
}

async fn initialize_user_table(db: &UserDb) -> Result<(), InitializeError> {
    create_user_table(db).await?;
    if !has_any_user(db).await? {
        insert_initial_user(db).await?;
        println!("Created initial user.");
    }
    Ok(())
}

#[rocket::async_trait]
impl Fairing for UserDbInitializer {
    fn info(&self) -> Info {
        Info {
            name: "User database initializer",
            kind: Kind::Ignite,
        }
    }

    async fn on_ignite(&self, rocket: Rocket<Build>) -> fairing::Result {
        let db = UserDb::fetch(&rocket).expect("User database is not loaded");
        initialize_user_table(db)
            .await
            .expect("Can't initialize user database");
        fairing::Result::Ok(rocket)
    }
}
